using LightCAD.MathLib;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml.Linq;
using static LightCAD.MathLib.Constants;


namespace LightCAD.Three
{
    public class Mesh : Object3D, IMorphTargets, IGeometry, IMaterialObject, ISolid
    {
        #region scope properties or methods

        //private static Matrix4 _inverseMatrix = new Matrix4();
        //private static Ray _ray = new Ray();
        //private static Sphere _sphere = new Sphere();
        //private static Vector3 _sphereHitAt = new Vector3();
        //private static Vector3 _vA = new Vector3();
        //private static Vector3 _vB = new Vector3();
        //private static Vector3 _vC = new Vector3();
        //private static Vector3 _tempA = new Vector3();
        //private static Vector3 _morphA = new Vector3();
        //private static Vector2 _uvA = new Vector2();
        //private static Vector2 _uvB = new Vector2();
        //private static Vector2 _uvC = new Vector2();
        //private static Vector3 _normalA = new Vector3();
        //private static Vector3 _normalB = new Vector3();
        //private static Vector3 _normalC = new Vector3();
        //private static Vector3 _intersectionPoint = new Vector3();
        //private static Vector3 _intersectionPointWorld = new Vector3();
        private static MeshContext getContext() =>
            ThreeThreadContext.GetCurrThreadContext().MeshCtx;
        private static Raycaster.Intersection checkIntersection(Object3D _object, Material material, Raycaster raycaster, Ray ray, Vector3 pA, Vector3 pB, Vector3 pC, Vector3 point)
        {
            Vector3 intersect;
            if (material.side == BackSide)
            {
                intersect = ray.IntersectTriangle(pC, pB, pA, true, point);
            }
            else
            {
                intersect = ray.IntersectTriangle(pA, pB, pC, (material.side == FrontSide), point);
            }

            if (intersect == null) return null;
            var ctx = getContext();
            var _intersectionPointWorld = ctx._intersectionPointWorld;
            _intersectionPointWorld.Copy(point);
            _intersectionPointWorld.ApplyMatrix4(_object.matrixWorld);
            var distance = raycaster.ray.Origin.DistanceTo(_intersectionPointWorld);
            if (distance < raycaster.near || distance > raycaster.far) return null;
            return new Raycaster.Intersection
            {
                distance = distance,
                point = _intersectionPointWorld.Clone(),
                target = _object

            };
        }
        private static Raycaster.Intersection checkGeometryIntersection(Mesh _object, Material material, Raycaster raycaster, Ray ray, BufferAttribute uv, BufferAttribute uv2, BufferAttribute normal, int a, int b, int c)
        {
            var ctx = getContext();
            var _vA = ctx._vA;
            var _vB = ctx._vB;
            var _vC = ctx._vC;
            var _uvA = ctx._uvA;
            var _uvB = ctx._uvB;
            var _uvC = ctx._uvC;
            var _intersectionPoint = ctx._intersectionPoint;
            var _normalA = ctx._normalA;
            var _normalB = ctx._normalB;
            var _normalC = ctx._normalC;

            _object.getVertexPosition(a, _vA);
            _object.getVertexPosition(b, _vB);
            _object.getVertexPosition(c, _vC);

            Raycaster.Intersection intersection = checkIntersection(_object, material, raycaster, ray, _vA, _vB, _vC, _intersectionPoint);
            var face = new Face3
            {
                a = a,
                b = b,
                c = c,
                normal = new Vector3(),
                materialIndex = 0
            };

            if (intersection != null)
            {

                if (uv?.count > 0)
                {

                    _uvA.FromBufferAttribute(uv, a);
                    _uvB.FromBufferAttribute(uv, b);
                    _uvC.FromBufferAttribute(uv, c);

                    intersection.uv = Triangle.GetInterpolation(_intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2());

                }

                if (uv2?.count > 0)
                {
                    _uvA.FromBufferAttribute(uv2, a);
                    _uvB.FromBufferAttribute(uv2, b);
                    _uvC.FromBufferAttribute(uv2, c);

                    intersection.uv2 = Triangle.GetInterpolation(_intersectionPoint, _vA, _vB, _vC, _uvA, _uvB, _uvC, new Vector2());

                }

                if (normal?.count > 0)
                {

                    _normalA.FromBufferAttribute(normal, a);
                    _normalB.FromBufferAttribute(normal, b);
                    _normalC.FromBufferAttribute(normal, c);

                    intersection.normal = Triangle.GetInterpolation(_intersectionPoint, _vA, _vB, _vC, _normalA, _normalB, _normalC, new Vector3());

                    if (intersection.normal.Dot(ray.Direction) > 0)
                    {

                        intersection.normal.MulScalar(-1);

                    }
                }
                Triangle.GetNormal(_vA, _vB, _vC, face.normal);
                intersection.face = face;
            }
            return intersection;
        }

        #endregion

        #region Properties

        public BufferGeometry geometry;
        public ListEx<Material> materials;

        #endregion

        #region properties
        public Material material { get { return this.materials[0]; } set { this.materials[0] = value; } }
        public ListEx<double> morphTargetInfluences { get; set; }
        public JsObj<int> morphTargetDictionary { get; set; }

        #endregion

        #region constructor
        public Mesh(BufferGeometry geometry = null, params Material[] material) : base()
        {
            var materials = material;
            if (geometry == null) geometry = new BufferGeometry();
            if (material == null) materials = new[] { new MeshBasicMaterial() };
            this.type = "Mesh";
            this.geometry = geometry;
            this.materials = materials.ToListEx();
            this.updateMorphTargets();
        }
        public Mesh(BufferGeometry geometry = null, Material material = null) : base()
        {
            ListEx<Material> materials = null;
            if (geometry == null) geometry = new BufferGeometry();
            if (material == null)
                materials = new ListEx<Material> { new MeshBasicMaterial() };
            else
                materials = new ListEx<Material> { material };
            this.type = "Mesh";
            this.geometry = geometry;
            this.materials = materials;
            this.updateMorphTargets();
        }
        #endregion

        #region methods
        public Mesh copy(Mesh source, bool recursive)
        {
            base.copy(source, recursive);
            if (source.morphTargetInfluences != null)
            {
                this.morphTargetInfluences = source.morphTargetInfluences.Slice();
            }
            if (source.morphTargetDictionary != null)
            {
                this.morphTargetDictionary = source.morphTargetDictionary.clone();
            }
            this.materials = source.materials;
            this.geometry = source.geometry;
            return this;
        }
        public override Object3D copy(Object3D source, bool recursive = true)
        {
            return copy(source as Mesh, recursive);
        }
        public override Object3D clone(bool recursive = true)
        {
            return new Mesh().copy(this, recursive);
        }
        public void updateMorphTargets()
        {
            var geometry = this.geometry;
            var morphAttributes = geometry.morphAttributes;
            var keys = morphAttributes.Keys.ToListEx();
            if (keys.Length > 0)
            {
                var morphAttribute = morphAttributes[keys[0]];
                if (morphAttribute != null)
                {
                    this.morphTargetInfluences = new ListEx<double>();
                    this.morphTargetDictionary = new JsObj<int>();
                    for (int m = 0, ml = morphAttribute.Length; m < ml; m++)
                    {
                        var name = morphAttribute[m]?.name ?? m.ToString();
                        this.morphTargetInfluences.Push(0);
                        this.morphTargetDictionary[name] = m;
                    }
                }
            }
        }
        public Vector3 getVertexPosition(int index, Vector3 target)
        {
            var geometry = this.geometry;
            var position = geometry.attributes.position;
            var morphPosition = geometry.morphAttributes.position;
            var morphTargetsRelative = geometry.morphTargetsRelative;
            target.FromBufferAttribute(position, index);
            var morphInfluences = this.morphTargetInfluences;
            if (morphPosition != null && morphInfluences != null)
            {
                var ctx = getContext();
                var _morphA = ctx._morphA;
                var _tempA = ctx._tempA;
                _morphA.Set(0, 0, 0);
                for (int i = 0, il = morphPosition.Length; i < il; i++)
                {
                    var influence = morphInfluences[i];
                    var morphAttribute = morphPosition[i];
                    if (influence == 0) continue;
                    _tempA.FromBufferAttribute(morphAttribute, index);
                    if (morphTargetsRelative)
                    {
                        _morphA.AddScaledVector(_tempA, influence);
                    }
                    else
                    {
                        _morphA.AddScaledVector(_tempA.Sub(target), influence);
                    }
                }
                target.Add(_morphA);
            }
            if (this is SkinnedMesh)
            {
                (this as SkinnedMesh).applyBoneTransform(index, target);
            }
            return target;
        }
        public override void raycast(Raycaster raycaster, ListEx<Raycaster.Intersection> intersects = null, object vars = null)
        {
            var geometry = this.geometry;
            var materials = this.materials;
            var matrixWorld = this.matrixWorld;
            if (materials == null) return;
            // Checking boundingSphere distance to ray
            if (geometry.boundingSphere == null) geometry.computeBoundingSphere();
            var ctx = getContext();
            var _sphere = ctx._sphere;
            _sphere.Copy(geometry.boundingSphere);
            _sphere.ApplyMatrix4(matrixWorld);
            var _ray = ctx._ray;
            _ray.Copy(raycaster.ray).Recast(raycaster.near);

            if (_sphere.ContainsPoint(_ray.Origin) == false)
            {
                var _sphereHitAt = ctx._sphereHitAt;
                if (_ray.IntersectSphere(_sphere, _sphereHitAt) == null) return;

                if (_ray.Origin.DistanceToSquared(_sphereHitAt) > Math.Pow((raycaster.far - raycaster.near), 2)) return;

            }
            //
            var _inverseMatrix = ctx._inverseMatrix;
            _inverseMatrix.Copy(matrixWorld).Invert();
            _ray.Copy(raycaster.ray).ApplyMatrix4(_inverseMatrix);
            // Check boundingBox before continuing
            if (geometry.boundingBox != null)
            {
                if (_ray.IntersectsBox(geometry.boundingBox) == false) return;
            }
            Raycaster.Intersection intersection;
            var index = geometry.index;
            var position = geometry.attributes.position;
            var uv = geometry.attributes.uv;
            var uv2 = geometry.attributes["uv2"];
            var normal = geometry.attributes.normal;
            var groups = geometry.groups;
            var drawRange = geometry.drawRange;
            if (index != null)
            {
                // indexed buffer geometry
                if (materials.Count > 1)
                {
                    for (int i = 0, il = groups.Length; i < il; i++)
                    {
                        var group = groups[i];
                        var groupMaterial = materials[group.MaterialIndex];
                        var start = Math.Max(group.Start, drawRange.start);
                        var end = Math.Min(index.count, Math.Min((group.Start + group.Count), (drawRange.start + drawRange.count)));
                        for (int j = start, jl = end; j < jl; j += 3)
                        {
                            var a = index.getIntX(j);
                            var b = index.getIntX(j + 1);
                            var c = index.getIntX(j + 2);

                            intersection = checkGeometryIntersection(this, groupMaterial, raycaster, _ray, uv, uv2, normal, a, b, c);

                            if (intersection != null)
                            {
                                intersection.faceIndex = (int)Math.Floor(j / 3.0); // triangle number in indexed buffer semantics
                                intersection.face.materialIndex = group.MaterialIndex;
                                intersects.Push(intersection);
                            }
                        }
                    }
                }
                else
                {
                    var start = Math.Max(0, drawRange.start);
                    var end = Math.Min(index.count, (drawRange.start + drawRange.count));
                    for (int i = start, il = end; i < il; i += 3)
                    {
                        var a = index.getIntX(i);
                        var b = index.getIntX(i + 1);
                        var c = index.getIntX(i + 2);
                        intersection = checkGeometryIntersection(this, material, raycaster, _ray, uv, uv2, normal, a, b, c);

                        if (intersection != null)
                        {
                            intersection.faceIndex = (int)Math.Floor(i / 3.0); // triangle number in indexed buffer semantics
                            intersects.Push(intersection);
                        }
                    }
                }
            }
            else if (position != null)
            {
                // non-indexed buffer geometry
                if (materials.Count > 1)
                {
                    for (int i = 0, il = groups.Length; i < il; i++)
                    {
                        var group = groups[i];
                        var groupMaterial = materials[group.MaterialIndex];
                        var start = Math.Max(group.Start, drawRange.start);
                        var end = Math.Min(position.count, Math.Min((group.Start + group.Count), (drawRange.start + drawRange.count)));
                        for (int j = start, jl = end; j < jl; j += 3)
                        {
                            var a = j;
                            var b = j + 1;
                            var c = j + 2;
                            intersection = checkGeometryIntersection(this, groupMaterial, raycaster, _ray, uv, uv2, normal, a, b, c);

                            if (intersection != null)
                            {
                                intersection.faceIndex = (int)Math.Floor(j / 3.0); // triangle number in non-indexed buffer semantics
                                intersection.face.materialIndex = group.MaterialIndex;
                                intersects.Push(intersection);
                            }
                        }
                    }
                }
                else
                {
                    var start = Math.Max(0, drawRange.start);
                    var end = Math.Min(position.count, (drawRange.start + drawRange.count));
                    for (int i = start, il = end; i < il; i += 3)
                    {
                        var a = i;
                        var b = i + 1;
                        var c = i + 2;
                        intersection = checkGeometryIntersection(this, material, raycaster, _ray, uv, uv2, normal, a, b, c);

                        if (intersection != null)
                        {
                            intersection.faceIndex = (int)Math.Floor(i / 3.0); // triangle number in non-indexed buffer semantics
                            intersects.Push(intersection);
                        }
                    }
                }
            }
        }


        public ListEx<Material> getMaterials()
        {
            return materials;
        }

        public void setMaterials(ListEx<Material> materials)
        {
            this.materials = materials;
        }
        public bool isMultiMaterial()
        {
            return this.materials.Count > 1;
        }

        public Material getMaterial()
        {
            return this.material;
        }

        public void setMaterial(Material material)
        {
            this.material = material;
        }

        public BufferGeometry getGeometry()
        {
            return this.geometry;
        }

        public void setGeometry(BufferGeometry geo)
        {
            this.geometry = geo;
        }
        #endregion
    }
}
